package xyz.lwm.lazycat.setting;

import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.yaml.snakeyaml.Yaml;
import xyz.lwm.lazycat.utility.CollectionUtil;
import xyz.lwm.lazycat.utility.MapUtil;
import xyz.lwm.lazycat.utility.PathUtil;
import xyz.lwm.lazycat.utility.StrPool;
import xyz.lwm.lazycat.utility.StringUtil;
import xyz.lwm.lazycat.utility.convert.Types;
import xyz.lwm.lazycat.utility.resource.Location;
import xyz.lwm.lazycat.utility.resource.Resource;
import xyz.lwm.lazycat.utility.scan.ScanUtil;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import static xyz.lwm.lazycat.setting.Setting.PROJECT_PATH;
import static xyz.lwm.lazycat.utility.StringUtil.isBlank;
import static xyz.lwm.lazycat.utility.scan.Scanner.FILE_PREFIX;

@Slf4j
abstract class SettingLoader {

    // 默认配置文件名称
    private static final String SETTING_FILE_NAME = "application";

    // profiles 文件配置, 如: setting.profiles=prod   则会扫描 application-prod.yml 用于覆盖配置
    private static final String PROFILES = "setting.profiles";

    private static final String YAML_SUBFIX = ".yaml";

    private static final String YML_SUBFIX = ".yml";

    private static final String PROPERTIES_SUBFIX = ".properties";

    private static final String JAVA_COMMAND  = "sun.java.command";

    private static final String JAR_SUBFIX  = ".jar";

    // 外部配置文件 app.properties 的路径, 配在启动参数里面, 包含文件名称和后缀, 文件名称可以自定义
    // --setting.location=classpath:app.properties 或 --setting.location=file:app.properties
    private static final String SETTING_LOCATION = "--setting.location";

    private static final Setting SETTING = load();

    @NotNull
    public static Setting get() {
        return SETTING;
    }

    /**
     * 获取全局 setting
     */
    private static Setting load() {
        SettingImpl setting = new SettingImpl();

        // 1. 系统变量
        loadSystemProperties().asMap().forEach(setting::set);
        log.info("load system properties");

        // 2. 配置文件变量
        // 2.1 内置父配置文件
        loadInnerSetting(SETTING_FILE_NAME + YAML_SUBFIX).asMap().forEach(setting::set);
        loadInnerSetting(SETTING_FILE_NAME + YML_SUBFIX).asMap().forEach(setting::set);
        loadInnerSetting(SETTING_FILE_NAME + PROPERTIES_SUBFIX).asMap().forEach(setting::set);

        // 2.2 内置子配置文件
        setting.get(PROFILES).also(profiles -> {
            log.info("setting profiles: {}", profiles);
            String profileName = SETTING_FILE_NAME + "-" + profiles;
            loadInnerSetting(profileName + YAML_SUBFIX).asMap().forEach(setting::set);
            loadInnerSetting(profileName + YML_SUBFIX).asMap().forEach(setting::set);
            loadInnerSetting(profileName + PROPERTIES_SUBFIX).asMap().forEach(setting::set);
        });

        // 2.2 外置配置文件
        Setting args = loadArgs();
        args.get(SETTING_LOCATION)
                .let(path -> disposePath(path, setting))
                .let(ScanUtil::scanResources)
                .getOr(Collections.emptyList())
                .stream().map(SettingLoader::loadResource)
                .map(Setting::asMap)
                .forEach(map -> map.forEach(setting::set));

        log.info("load settings");

        // 3.启动参数覆盖变量
        args.asMap().forEach(setting::set);

        return setting;
    }

    private static String disposePath(String path, Setting setting) {
        if (path.startsWith(FILE_PREFIX)) {
            String location = path.substring(FILE_PREFIX.length());
            // 相对路径, 则拼接项目路径
            if (location.startsWith(".") && setting.get(PROJECT_PATH).isPresent()) {
                location = setting.get(PROJECT_PATH).get() + location.substring(1);
            }
            return location;
        }
        return path;
    }

    /**
     * 获取环境变量对象, 创建环境变量对象并加载文件
     * 只加载 .properties , .yaml , .yml  的文件
     */
    @NotNull
    static Setting load(String path, Location location) {
        return ScanUtil.scanResources(location.toPattern(path)).stream()
                .findFirst().map(SettingLoader::loadResource).orElse(new SettingImpl());
    }

    private static Setting loadArgs() {
        SettingImpl setting = new SettingImpl();
        String cmd = System.getProperty(JAVA_COMMAND);
        if (StringUtil.isNotBlank(cmd)) {
            String[] args = cmd.split(StrPool.STR_SPACE);
            if (CollectionUtil.isNotEmpty(args)) {
                for (String arg : args) {
                    int i = arg.indexOf("=");
                    if (i > 2 && arg.startsWith("--")) {
                        String key = arg.substring(2, i);
                        String val = arg.substring(i + 1);
                        setting.set(key, val);
                    } else {
                        String present = "";
                        setting.set(arg, present);
                        setProjectPath(setting, arg);
                    }
                }
            }
        }
        return setting;
    }

    private static void setProjectPath(SettingImpl setting, String arg) {
        if (arg.endsWith(JAR_SUBFIX) && arg.lastIndexOf("/") > 0) {
            String path = PathUtil.normalizePath(arg);
            setting.set(PROJECT_PATH, path.substring(0, path.lastIndexOf("/")));
        }
    }

    private static Setting loadSystemProperties() {
        SettingImpl setting = new SettingImpl();
        System.getProperties().forEach((k, v) ->
                setting.set(String.valueOf(k), v));
        return setting;
    }

    /**
     * 获取环境变量对象, 创建环境变量对象并加载文件
     * 只加载 .properties , .yaml , .yml  的文件
     */
    private static Setting loadInnerSetting(String fileName) {
        List<Resource> resources = ScanUtil.scanResources(Location.CLASSPATH.toPattern(fileName));
        if (CollectionUtil.isEmpty(resources)) {
            return new SettingImpl();
        }
        return loadResource(resources.iterator().next());
    }

    static Setting loadResource(Resource resource) {
        SettingImpl setting = new SettingImpl();
        if (resource == null) {
            return setting;
        }
        String path = resource.getPath();
        if (path.endsWith(PROPERTIES_SUBFIX)) {
            loadPropertiesResource(setting, resource);
        } else if (path.endsWith(YAML_SUBFIX) || path.endsWith(YML_SUBFIX)) {
            loadYamlResource(setting, resource);
        }
        return setting;
    }

    private static void loadPropertiesResource(SettingImpl setting, Resource resource) {
        try (InputStream inputStream = resource.getInputStream()) {
            Properties properties = new Properties();
            properties.load(inputStream);
            properties.forEach((k, v) -> setting.set(String.valueOf(k), v));
        } catch (IOException e) {
            log.error("load properties error", e);
        }
    }

    private static void loadYamlResource(SettingImpl setting, Resource resource) {
        try (InputStream inputStream = resource.getInputStream()) {
            Yaml yaml = new Yaml();
            Map<String, Object> ymlMap = yaml.load(inputStream);
            if (MapUtil.isNotEmpty(ymlMap)) {
                Properties properties = new Properties();
                flattenMap(null, ymlMap, properties);
                properties.forEach((k, v) -> setting.set(String.valueOf(k), v));
            }
        } catch (IOException e) {
            log.error("load yaml error", e);
        }
    }

    @SuppressWarnings("unchecked")
    private static void flattenMap(String prefix, Map<String, Object> map, Properties properties) {
        map.forEach((k, v) -> {
            if (k != null && v != null) {
                String key = isBlank(prefix) ? k : prefix + "." + k;
                if (v instanceof Map) {
                    // nest map, recursive processing
                    flattenMap(key, (Map<String, Object>) v, properties);
                } else if (v instanceof List) {
                    // list, use index
                    List<?> list = (List<?>) v;
                    for (int i = 0; i < list.size(); i++) {
                        Object item = list.get(i);
                        String listKey = key + "[" + i + "]";
                        if (item != null && Types.isBaseType(item.getClass())) {
                            properties.setProperty(listKey, item.toString());
                        } else if (item instanceof Map) {
                            Map<String, Object> itemMap = (Map<String, Object>) item;
                            flattenMap(listKey, itemMap, properties);
                        }
                    }
                } else {
                    // base type, set value directly
                    properties.setProperty(key, v.toString());
                }
            }
        });
    }

}
